Skip to main content

400. More Building Blocks

Intro

  • In addition to Modules, Controllers and Providers, nest has other building blocks it uses in its architecture to make nest extendable and maintainable, there are
    • Exception filters
      • specialized class that intercepts and handles uncaught exceptions thrown within the application.
      • It enables centralized error handling, customizable error responses.
    • Guards
      • control access to route handlers based on custom criteria, like user roles or permissions.
    • Interceptors
      • Enable aspect-oriented programming with functionality like response transformation, error handling, and caching
    • pipes
      • Transform input data or validate request payloads before reaching route handlers.
    • And middleware
      • Custom functions that execute before reaching the main route handler. Middleware can perform tasks like logging, authentication, and data manipulation.

How and Why to Use Them ?

Exception Filters

  • Exception filters | NestJS - A progressive Node.js framework
  • can be used for?
    • Centralized error handling: Exception filters allow you to handle all errors in a centralized location, rather than scattering error handling logic throughout your application. This leads to cleaner, more maintainable code.
    • Custom error responses: By using exception filters, you can customize the error response sent to the client.
    • Logging and monitoring: Exception filters provide a single point where you can log and monitor errors that occur in your application.
    • Separation of concerns: Exception filters help separate error handling logic from the main application logic.
  • nest g filter path/filter-name
  • Throwing standard exception
//in nest js you can throw a standard exception from your endpoints like this.
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}

response = {
statusCode: 403,
message: "Forbidden"
}

Creating Exception Filters

@Catch(HttpException) //<--- catching HttpException only
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();

response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(), //new field
path: request.url, //new field
});
}
}
  • Use it at method level, applies for the method's response only
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}

//new updated response
response = {
statusCode: 403,
timestamp: "'2023-07-11T09:22:03.242Z'",
path: "request path"
}
  • use it at controller level, applies for all methods' response inside the controller file
@UseFilters(new HttpExceptionFilter())
export class CatsController {}
  • use it at module level, applies for all exception responses found in that module's controllers
@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
  • use it inside main.ts, applies across the app entirely
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();

Guards

If you are looking for a real-world example on how to implement an authentication mechanism in your application, check Authentication | NestJS - A progressive Node.js framework. Likewise, for more sophisticated authorization example, check Authorization | NestJS - A progressive Node.js framework

Creating Custom API Guard

  1. create a new decorator with setMetaData
  2. use the decorator inside a controller's method or class
  3. use the metadata information inside guards logic
  4. introduce your new guard to nest at specific level
import { SetMetadata } from "@nestjs/common";

export const IS_PUBLIC_KEY = 'isPublic';

export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
@Controller('coffes')
export class CoffesController {
constructor(private readonly coffesService: CoffesService) {}

@Public() //custom decorator
@Get()
findAll(@Query() paginationQuery: PaginationDTO) {
return this.coffesService.findAll(paginationQuery);
}

@Get(':id')
findOne(@Param('id') id: string) {
return this.coffesService.findOne(id);
}
@Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}

canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const isPublic = this.reflector.get<boolean>(
IS_PUBLIC_KEY,
context.getHandler(),
);

if (isPublic) {
return true;
}

const request = context.switchToHttp().getRequest();
const authHeader = request.header('Authorization');
return authHeader === '123';
}
}
//registering our new guard inside main.ts
app.useGlobalGuards(new ApiKeyGuard());

Pipes

@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}

//if id is invalid the following response will be returned
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}

Interceptors

  • Interceptors | NestJS - A progressive Node.js framework
  • nest g interceptor path/interceptor-name
  • Can be used for?
    • Add extra logic before or after method execution
      • add logging, persist request time diff…
    • Transform a result returned from a method
      • i.e. wrap response in {data: response}
    • transform exception thrown from a method
    • cache responses
    • terminate a request after x-seconds if no response

Where Can We Use These Blocks?

  • You can use filters, guards, interceptors and pipes at different levels in nest, levels being controller, method, module and global.
  • You need to use different syntax at each level
    • global(inside main.ts)
      • app.useGlobalFilter(new FilterName())
      • app.useGlobalGuards(new GuardName())
      • app.useGlobalInterceptors(new IntercepterName())
      • app.useGlobalPipes(new PipeName())
    • module
      • when you use a building blocks in a module level, it will be available to all components the module define.
        • ex: {provide: APP_FILTER, useClass: FilterName }
    • Controller
      • no need to use new keyword
      • @UsePipes(FilterName)
      • @UseGuards(GuardName)
      • @UseIntercepters(IntercepterName)
      • @UseFilters(FilterName)
    • method
      • same as controller
    • parameter
      • only pipes work at parameter level
      • i.e. @Body(ValidationPipe)

Middleware

  • Middleware | NestJS - A progressive Node.js framework
  • Nest middleware are, by default, equivalent to express middleware.
  • Can be used for to?
    • execute any code before/after request.
    • Make changes to the request and the response objects.
    • End the request-response cycle.
    • Call the next middleware function in the stack.

Create New Middleware

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
}

Using a Middleware

  • Using middleware | NestJS - A progressive Node.js framework
  • There is no place for middleware in the @Module() decorator. Instead, we set them up using the configure() method of the module class.
  • Modules that include middleware have to implement the NestModule interface. Let's set up the LoggerMiddleware at the AppModule level.
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('cats');
}
}

Global Middleware

  • If we want to bind middleware to every registered route at once, we can use the use() method that is supplied by the INestApplication instance:
const app = await NestFactory.create(AppModule);
app.use(logger);

//or app.use(cron())
await app.listen(3000);
//doesn't allow dependency injection
//or - this if you want dependency injection
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('*'); <--- the main thing
}
}